-module(test_executor).
-export([start/4, stop/0, click/2, update_state/1, stop/1, init/4]).
-include("definitions.hrl").

-spec start/4 :: (DeviceList, Tests, MyName, State) -> ok
  when
  DeviceList :: [device()],
  Tests :: dict:dict(device_id(), test_id()),
  MyName :: atom(),
  State :: works | stays | stops.

start(DeviceList, Tests, MyName, State) ->
  case whereis(MyName) of
    undefined -> ok;
    Pid when is_pid(Pid) ->
      erlang:monitor(process, Pid),
      receive
        {'DOWN', _, process, _, Pid} ->
          ok
      end
  end,

  {P, _} = spawn_monitor(test_executor, init, [DeviceList, Tests, self(), State]),

  yes = global:register_name(MyName, P),
  receive
    {ok, testExecutorStarted} ->
      test_executor_supervisor:i_am_reborned(),
      io:format("~p ~p is alive ~n", [MyName, self()])
  end,
  ok.

-spec init/4 :: (DeviceList, Tests, Pid, State) -> ok
  when
  DeviceList :: [device()],
  Tests :: dict:dict(device_id(), test_id()),
  Pid :: pid(),
  State :: works | stays | stops.

init(DeviceList, Tests, Pid, State) ->

  if
    State =:= works ->
      Start = os:timestamp(),
      TestingState = #testingState{devicesCount = 0, devicesFinished = 0, reported = false, finish = Start, start = Start},
      {Workers, Pids} = run_on(DeviceList, Tests, dict:new(), dict:new()),
      Pid ! {ok, testExecutorStarted},
      event_loop(Workers, Pids, Tests, TestingState, DeviceList, State);

  %% State =:= stays or State =:= stops ->
    true ->
      ExState = #testingState{},
      {Workers, Pids} = {dict:new(), dict:new()},
      Pid ! {ok, testExecutorStarted},
      event_loop(Workers, Pids, Tests, ExState, DeviceList, State)
  end.


-spec click/2 :: (DeviceId, TestId) -> Color
  when
  DeviceId :: device_id(),
  TestId :: test_id(),
  Color :: red | green | blue.

click(DeviceId, TestId) ->
  Atom = test_executor_supervisor:get_test_executor(),
  global:whereis_name(Atom) ! {click, DeviceId, TestId, self()},
  receive
    {clicked, works} -> green;
    {clicked, error} -> red;
    {clicked, paused} -> blue
  end.

-spec update_state/1 :: (state()) -> ok.
update_state(NewState) ->
  Atom = test_executor_supervisor:get_test_executor(),
  case global:whereis_name(Atom) of
    undefined ->
      error("dead test executor");
    Pid when is_pid(Pid) ->
      Pid ! {update, NewState, self()}
  end,
  ok.

%% остановить процесс, занятый данным прибором
-spec stop_worker/3 :: (DeviceId, Workers, PidWhoStopped) -> {clicked, paused}
  when
  DeviceId :: device_id(),
  Workers :: dict:dict(device_id(), pid()),
  PidWhoStopped :: pid().

stop_worker(DeviceId, Workers, PidWhoStopped) ->
  WorkerPid = dict:fetch(DeviceId, Workers),
  ok = worker:stop(WorkerPid),
  PidWhoStopped ! {clicked, paused}.

-spec stop_workers/2 :: (WorkersToStop, Workers) -> ok
  when
  WorkersToStop :: [device_id()],
  Workers :: dict:dict(device_id(), pid()).

stop_workers([], _) ->
  ok;
stop_workers([W | Rest], Workers) ->
  Worker = dict:fetch(W, Workers),
  ok = worker:stop(Worker),
  stop_workers(Rest, Workers).

-spec stop/0 :: () -> ok.
stop() ->
  Atom = test_executor_supervisor:get_test_executor(),
  try global:whereis_name(Atom) of
    undefined -> ok;
    Pid -> Pid ! {stop, self()},
      receive
        {testExecutor_stopped, ok} ->
          io:format("testExecutor ~p finished~n", [Atom]),
          ok
      end
  catch
    error:_ ->
      io:format("testExecutor is already finished~n"),
      ok
  end.

-spec stop/1 :: (atom()) -> ok.
stop(Atom) ->
  try global:whereis_name(Atom) of
    undefined -> ok;
    Pid -> Pid ! {stop, self()},
      receive
        {testExecutor_stopped, ok} ->
          io:format("testExecutor finished~n"),
          ok
      end
  catch
    error:_ ->
      io:format("testExecutor is already finished~n"),
      ok
  end.

-spec event_loop/6 :: (Workers, Pids, Tests, ExecutorState, Devices, State) -> ok
  when
  Workers :: dict:dict(device_id(), pid()),
  Pids :: dict:dict(pid(), device_id()),
  Tests :: dict:dict(device_id(), test_id()),
  ExecutorState :: #testingState{},
  Devices :: [device()],
  State :: works | stays | stops.


event_loop(Workers, Pids, Tests, ExecutorState, Devices, State) ->
  receive
    {ok, updatedState} ->
      event_loop(Workers, Pids, Tests, ExecutorState, Devices, State);

    {update, DeviceState, From} ->
      if
        State =:= works ->
          {memory, MemoryAllocated} = erlang:process_info(self(), memory),
          {ok, MaxMemoryAllocated} = application:get_env(test_executor_max_memory),
          NewState = case MemoryAllocated > MaxMemoryAllocated of%в байтах
                       true ->
                         test_executor_supervisor:shift(Workers, Pids, ExecutorState),
                         stays;
                       _ ->
                         State
                     end,
          storage_supervisor:update_state(DeviceState, From),
          event_loop(Workers, Pids, Tests, ExecutorState, Devices, NewState);

        true ->
          storage_supervisor:update_state(DeviceState, From),
          event_loop(Workers, Pids, Tests, ExecutorState, Devices, State)
      end;

  %%dict:dict(integer(), pid()),%Workers[DeviceId]->Pid
  %%dict:dict(pid(), integer()),%%Pids[Pid]->DeviceId
    {start_monitor_workers, WorkersNew, PidsNew, TestingState, FromName} when is_atom(FromName) ->

      %{_, Secs, MicroSecs} = os:timestamp(),
%%       io:format("~nregister  "),
      ok = monitor_list(dict:fetch_keys(PidsNew)),
      %io:format("active From: ~p  ~p ~p~n", [FromName, Secs, MicroSecs]),
      FromName ! {monitored, self(), FromName},
      event_loop(WorkersNew, PidsNew, Tests, TestingState, Devices, works);

    {'DOWN', Ref, process, FromPid, Reason} -> %%Reference
      erlang:demonitor(Ref),
      case State of
        works ->
          handle_exit_signal(Workers, Pids, Tests, ExecutorState, Devices, State, FromPid, Reason);

        _ -> %% не обрабатывать падение
%%           {_, Name} = erlang:process_info(self(), registered_name),
%%           io:format("Me: ~p / ~p, state = ~p, got down from ~p~n", [Name, self(), State, FromPid]),
          event_loop(Workers, Pids, Tests, ExecutorState, Devices, State)
      end;

    {monitored, _, MyName} when is_atom(MyName) ->
      test_executor_supervisor:i_am_dead(),
      event_loop(Workers, Pids, Tests, ExecutorState, Devices, stops);

    {click, DeviceId, TestId, PidWhoStopped} ->
      case dict:is_key(DeviceId, Workers) of
        true ->
          stop_worker(DeviceId, Workers, PidWhoStopped),%% остановили
          event_loop(Workers, Pids, Tests, ExecutorState, Devices, State);

        false ->
          TestsNew = dict:store(DeviceId, TestId, Tests),
          Device = lists:filter(fun(El) -> El#deviceDescriptor.id =:= DeviceId end, Devices),
          StateForDev = storage_supervisor:get_last_for_device(DeviceId),
          case StateForDev#stateRecord.unexpected_answer of
            true ->
              PidWhoStopped ! {clicked, error},
              event_loop(Workers, Pids, TestsNew, ExecutorState, Devices, State);
            false ->
              {WorkersNew, PidsNew} = run_on_after_fault(Device, TestsNew, Workers, Pids),
              PidWhoStopped ! {clicked, works}, %% запустили
              event_loop(WorkersNew, PidsNew, TestsNew, ExecutorState, Devices, State)
          end
      end;

    {stop, Pid} ->
      DevicesIds = dict:fetch_keys(Workers),
      ok = stop_workers(DevicesIds, Workers),
      Pid ! {testExecutor_stopped, ok},
      ok;

    Event -> io:format("test executor: event : ~p~n", [Event]),
      event_loop(Workers, Pids, Tests, ExecutorState, Devices, State)

  after 100 ->
    case State of
      works ->
        event_loop(Workers, Pids, Tests, ExecutorState, Devices, State);
      stays ->
        event_loop(Workers, Pids, Tests, ExecutorState, Devices, State);
      _ ->
        reborn()
    end
  end.

reborn() ->
  flush(),
  {_, MyName} = erlang:process_info(self(), registered_name),
%%   true = unregister(MyName),
  {_, Secs1, MicroSecs1} = os:timestamp(),
  io:format("finished! Me: ~p  ~p   ~p~n", [MyName, Secs1, MicroSecs1]),
  garbage_collect(self()),
  spawn(test_executor_supervisor, reborn_me, [MyName]).

-spec handle_exit_signal/8 :: (

    dict:dict(integer(), pid()),%Workers[DeviceId]->Pid
    dict:dict(pid(), integer()),%%Pids[Pid]->DeviceId
    dict:dict(integer(), integer()),%device -> test
    #testingState{},
    [device()],
    atom(),
    pid(),
    normal | test_cancelled | errorDevice | unexpectedAnswer | noproc
) -> ok.

handle_exit_signal(Workers, Pids, Tests, ExecutorState, Devices, State, FromPid, Reason) ->
%io:format("~n fell: ~p  ", [pid_to_list(FromPid)]),
%io:format("~p~n", [Reason]),
  case Reason of%проверить, надо ли поднимать
    normal ->
      {Wnew, Pnew, NewExecutorState} = handle_normal_ending(Workers, Pids, Tests, ExecutorState, FromPid),
      event_loop(Wnew, Pnew, Tests, NewExecutorState, Devices, State);

    test_cancelled ->
      {Wnew, Pnew, NewExecutorState} = handle_normal_ending(Workers, Pids, Tests, ExecutorState, FromPid),
      event_loop(Wnew, Pnew, Tests, NewExecutorState, Devices, State);

    unexpectedAnswer ->
      {Wnew, Pnew, NewExecutorState} = handle_normal_ending(Workers, Pids, Tests, ExecutorState, FromPid),
      event_loop(Wnew, Pnew, Tests, NewExecutorState, Devices, State);

    noproc ->
%%       {_, B} = erlang:process_info(self(), registered_name),
%%       io:format("noproc end pid ~p Me: ~p  ~n", [FromPid, B]),
      %{Wnew, Pnew, NewExecutorState} = handle_normal_ending(Workers, Pids, Tests, ExecutorState, FromPid),
      %event_loop(Wnew, Pnew, Tests, NewExecutorState, Devices, Queue, State);
      restore(Workers, Pids, Tests, ExecutorState, Devices, State, FromPid);

    errorDevice ->
%%       {_, B} = erlang:process_info(self(), registered_name),
%%       io:format("error dev end pid ~p Me: ~p  ", [FromPid, B]),
      restore(Workers, Pids, Tests, ExecutorState, Devices, State, FromPid)
  end.


%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
%                                       перезапуск
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%


-spec handle_normal_ending/5 :: (
    dict:dict(device_id(), pid()),%Workers
    dict:dict(pid(), device_id()),%Pids by device
    dict:dict(device_id(), test_id()),%device -> test
    #testingState{},
    pid()) ->
  {dict:dict(device_id(), pid()),
    dict:dict(pid(), device_id()),
    #testingState{}}.

handle_normal_ending(Workers, Pids, _, ExecutorState, FromPid) ->
  Device = dict:fetch(FromPid, Pids),
  Wnew = dict:erase(Device, Workers),

%%   {_, MyName} = erlang:process_info(self(), registered_name),
%%   io:format("dict:erase: F: ~p, Pid = ~p, Me = ~p~n", [handle_normal_ending, FromPid, MyName]),
%%   io:format("~p~n", [handle_normal_ending]),
  Pnew = dict:erase(FromPid, Pids),
  Size = dict:size(Wnew),
  storage_supervisor:flush(),
  if
    Size == 0 ->% and not ExecutorState#testingState.reported
      Finish = os:timestamp(),
      NewExState = ExecutorState#testingState{reported = true, finish = Finish},
      Start = ExecutorState#testingState.start,
      io:format(ExecutorState#testingState.message, [timer:now_diff(Finish, Start) div 1000]),
      info_ui:stats(),
      {Wnew, Pnew, NewExState};
    true ->
      {Wnew, Pnew, ExecutorState}
  end.


-spec run_on/4 :: ([device()],
    dict:dict(device_id(), test_id()),%device -> test
    dict:dict(device_id(), pid()),%Workers[DeviceId]->Pid
    dict:dict(pid(), device_id())%%Pids[Pid]->DeviceId
) ->
  {
    dict:dict(device_id(), pid()),%Workers[DeviceId]->Pid
    dict:dict(pid(), device_id())%%Pids[Pid]->DeviceId
  }.
run_on([], _, Workers, Pids) ->
  io:nl(),
  {Workers, Pids};

run_on([Device | DevicesTail], Tests, Workers, Pids) ->
  DeviceId = Device#deviceDescriptor.id,
  {WorkersNew, PidsNew} = case dict:is_key(DeviceId, Tests) of
                            true ->
                              TestId = dict:fetch(DeviceId, Tests),
                              DevicePid = Device#deviceDescriptor.driver,
                              {ok, Stop} = application:get_env(stop_at_unexpected_answer),
                              NewPid = spawn(worker, start, [DeviceId, TestId, DevicePid, false, Stop]),
                              _ = erlang:monitor(process, NewPid),%%Ref
%%                               io:format("~p ", [NewPid]),
                              {dict:store(DeviceId, NewPid, Workers), dict:store(NewPid, DeviceId, Pids)};
                            false -> {Workers, Pids}%тест не назначен
                          end,
  run_on(DevicesTail, Tests, WorkersNew, PidsNew).

-spec run_on_after_fault/4 :: (
    [device()],
    dict:dict(device_id(), test_id()),%device -> test
    dict:dict(device_id(), pid()),%Workers[DeviceId]->Pid
    dict:dict(pid(), device_id())%%Pids[Pid]->DeviceId
) ->
  {
    dict:dict(device_id(), pid()),%Workers[DeviceId]->Pid
    dict:dict(pid(), integer())%%Pids[Pid]->DeviceId
  }.

run_on_after_fault([], _, Workers, Pids) ->
  {Workers, Pids};

run_on_after_fault([Device | DevicesTail], Tests, Workers, Pids) ->
  DeviceId = Device#deviceDescriptor.id,
  {WorkersNew, PidsNew} = case dict:is_key(DeviceId, Tests) of
                            true ->
                              TestId = dict:fetch(DeviceId, Tests),
                              DevicePid = Device#deviceDescriptor.driver,
                              {ok, Stop} = application:get_env(stop_at_unexpected_answer),
                              {NewPid, _} = spawn_monitor(worker, start, [DeviceId, TestId, DevicePid, true, Stop]),
%%                               io:format("~p ~p~n", [run_on_after_fault, NewPid]),
                              {dict:store(DeviceId, NewPid, Workers), dict:store(NewPid, DeviceId, Pids)};
                            false ->
                              io:format("test not assigned ~p~n", [DeviceId]),
                              {Workers, Pids}%тест не назначен
                          end,
  run_on_after_fault(DevicesTail, Tests, WorkersNew, PidsNew).

-spec restore/7 :: (Workers, Pids, Tests, ExecutorState, Devices, State, FromPid
) -> ok
  when
  Workers :: dict:dict(device_id(), pid()),
  Pids :: dict:dict(pid(), device_id()),
  Tests :: dict:dict(device_id(), test()),
  ExecutorState :: #testingState{},
  Devices :: [device()],
  State :: atom(),
  FromPid :: pid().

restore(Workers, Pids, Tests, ExecutorState, Devices, State, FromPid) ->
  case dict:is_key(FromPid, Pids) of
    true ->
      DeviceId = dict:fetch(FromPid, Pids),
      StateDevice = worker:get_restored_state_without_cache(DeviceId, dict:fetch(DeviceId, Tests)),
      PNew = dict:erase(FromPid, Pids),
      WNew = dict:erase(DeviceId, Workers),
%%       {_, MyName} = erlang:process_info(self(), registered_name),
%%       io:format("dict:erase: F: ~p, Pid = ~p, Me = ~p  ", [restore, FromPid, MyName]),
      case StateDevice#stateRecord.completed of
        true ->
          event_loop(WNew, PNew, Tests, ExecutorState, Devices, State);
        false ->
          Device = lists:filter(fun(#deviceDescriptor{id = N}) -> N =:= DeviceId end, Devices),
%%           L = length(Device),
%%           if
%%             L == 0 ->
%%               io:format("  device len = ~p pid = ~p  N = ~p  ~n", [length(Devices), FromPid, DeviceId]),
%%               lists:foreach(fun(El) -> io:format("~p ", [El]) end, Devices),
%%               io:nl();
%%             true -> ok
%%           end,

          {WorkersNew, PidsNew} = run_on_after_fault(Device, Tests, WNew, PNew),
          event_loop(WorkersNew, PidsNew, Tests, ExecutorState, Devices, State)
      end;

    false ->
      event_loop(Workers, Pids, Tests, ExecutorState, Devices, State)
  end.

%% примониториться к списку процессов
-spec monitor_list/1 :: (list(pid())) -> ok.
monitor_list([]) ->
%%   {_, MyName} = erlang:process_info(self(), registered_name),
%%   io:format("~p~n", [MyName]),
  ok;

monitor_list([Pid | Pids]) ->
  _ = erlang:monitor(process, Pid),
%%   io:format("~p ", [Pid]),
  monitor_list(Pids).

flush() ->
  receive
    Any ->
      io:format("testExecutor flush received: ~p~n", [Any]),
      flush()
  after
    0 ->
      true
  end.